<!-- Admin Jobs Tab -->
<div id="tabAdminJobs" class="tab-content" role="tabpanel">
    <div id="adminJobs_topbar" class="text-muted" style="margin: 8px 0; display:flex; align-items:center; gap:10px;">
        <span>No recent actions</span>
        <a href="/docs-static/Code_Documentation/Jobs_Module.md" target="_blank" rel="noopener" class="btn btn-link btn-sm" style="text-decoration:none;">Jobs Docs</a>
        <button class="btn btn-secondary btn-sm" id="btnAdminOpenRotateKeys" title="Rotate encrypted payload/result keys (dry-run or execute)">Rotate Keys…</button>
    </div>
    <div class="endpoint-section" id="adminJobsStats">
        <h2>
            <span class="endpoint-method get">GET</span>
            <span class="endpoint-path">/api/v1/jobs/stats - Jobs Queue Stats</span>
            <a href="/docs-static/Code_Documentation/Jobs_Admin_Examples.md" target="_blank" rel="noopener" style="margin-left:10px; font-size: 0.9em;">Examples</a>
        </h2>
        <p>Queue depth and processing counts per domain/queue/job_type. Supports auto-refresh.</p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="adminJobs_domain">Domain:</label>
                    <input type="text" id="adminJobs_domain" placeholder="e.g., chatbooks">
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminJobs_queue">Queue:</label>
                    <select id="adminJobs_queue">
                        <option value="">(any)</option>
                        <!-- options populated dynamically from /api/v1/config/jobs -->
                    </select>
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminJobs_jobType">Job Type:</label>
                    <input type="text" id="adminJobs_jobType" placeholder="e.g., export">
                </div>
            </div>
        </div>

        <div class="metrics-controls">
            <button class="btn btn-primary" id="btnAdminJobsRefresh">Refresh</button>
            <button class="btn btn-secondary" id="btnAdminJobsAutoRefresh">Auto-Refresh (10s)</button>
            <button class="btn btn-secondary" id="btnAdminJobsStop">Stop</button>
        </div>
        <div style="display:flex; align-items:center; gap:8px; margin-top:8px;">
            <div id="adminJobs_savedFiltersBadge" style="display:inline-block; padding:4px 8px; border:1px solid var(--color-border); border-radius:12px; font-size:12px; color:var(--color-text-secondary);">
                Saved Filters: (none)
            </div>
            <button id="adminJobs_resetFilters" class="btn btn-secondary btn-sm" style="padding:2px 8px; border-radius:12px; font-size:12px;" title="Clear saved filters and reset inputs">Reset Filters</button>
        </div>

        <div class="table-responsive mt-2">
            <table class="data-table">
                <thead>
                    <tr>
                        <th>Domain</th>
                        <th>Queue</th>
                        <th>Job Type</th>
                        <th>Queued (ready)</th>
                        <th>Scheduled</th>
                        <th>Processing</th>
                        <th>Quarantined</th>
                    </tr>
                </thead>
                <tbody id="adminJobs_tableBody">
                    <tr><td colspan="7" class="text-muted">No data</td></tr>
                </tbody>
            </table>
        </div>
    </div>

    <!-- Prune Jobs -->
    <div class="endpoint-section" id="adminJobsPrune">
        <h2>
            <span class="endpoint-method post">POST</span>
            <span class="endpoint-path">/api/v1/jobs/prune - Prune Jobs</span>
            <a href="/docs-static/Code_Documentation/Jobs_Admin_Examples.md" target="_blank" rel="noopener" style="margin-left:10px; font-size: 0.9em;">Examples</a>
        </h2>
        <p>
            Delete jobs by status older than a threshold. Use with caution in production.
            <span title="Destructive actions require X-Confirm: true header when not using Dry Run." style="margin-left:8px; color: var(--color-text-secondary); cursor: help;">(Requires X-Confirm for real runs)</span>
        </p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label>Statuses to prune:</label>
                    <div class="checkbox-group">
                        <label><input type="checkbox" class="adminJobs_status" value="completed" checked> completed</label>
                        <label><input type="checkbox" class="adminJobs_status" value="failed" checked> failed</label>
                        <label><input type="checkbox" class="adminJobs_status" value="cancelled" checked> cancelled</label>
                        <label><input type="checkbox" class="adminJobs_status" value="queued"> queued</label>
                        <label><input type="checkbox" class="adminJobs_status" value="processing"> processing</label>
                    </div>
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminJobs_olderDays">Older Than (days):</label>
                    <input type="number" id="adminJobs_olderDays" value="30" min="1" max="3650">
                </div>
                <div class="form-group">
                    <label><input type="checkbox" id="adminJobs_dryRun" checked> Dry Run (count only)</label>
                </div>
                <div id="adminJobsPrune_warning" style="display:none; margin:4px 0; padding:6px 8px; border:1px solid #b45309; border-radius:6px; color:#7c2d12; background:#fff7ed; font-size:12px;">
                    Warning: This will permanently delete matching jobs. Ensure filters are correct and include <code>X-Confirm: true</code>.
                </div>
                <div class="form-group">
                    <button id="adminJobsPrune_btn" class="btn btn-warning" title="Requires X-Confirm: true header when Dry Run is unchecked">Prune Jobs</button>
                </div>
            </div>
        </div>

        <h3>Response:</h3>
        <div id="adminJobsPrune_summary" class="text-muted" style="margin-bottom:6px;">-</div>
        <pre id="adminJobsPrune_result">---</pre>
    </div>

    <!-- TTL Sweep -->
    <div class="endpoint-section" id="adminJobsTTL">
        <h2>
            <span class="endpoint-method post">POST</span>
            <span class="endpoint-path">/api/v1/jobs/ttl/sweep - TTL Sweep</span>
            <a href="/docs-static/Code_Documentation/Jobs_Admin_Examples.md" target="_blank" rel="noopener" style="margin-left:10px; font-size: 0.9em;">Examples</a>
        </h2>
        <p>
            Cancel or fail jobs that exceed a max age (queued) or runtime (processing). Scoped by current filters.
            <span title="Destructive actions require X-Confirm: true header." style="margin-left:8px; color: var(--color-text-secondary); cursor: help;">(Requires X-Confirm)</span>
        </p>
        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="adminJobs_ttl_age">Max Age (seconds) for queued:</label>
                    <input type="number" id="adminJobs_ttl_age" placeholder="e.g., 86400">
                </div>
                <div class="form-group">
                    <label for="adminJobs_ttl_runtime">Max Runtime (seconds) for processing:</label>
                    <input type="number" id="adminJobs_ttl_runtime" placeholder="e.g., 7200">
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminJobs_ttl_action">Action:</label>
                    <select id="adminJobs_ttl_action">
                        <option value="cancel" selected>cancel</option>
                        <option value="fail">fail</option>
                    </select>
                </div>
                <div class="form-group">
                    <button id="adminJobsTTL_btn" class="btn btn-warning" title="Requires X-Confirm: true header">Run TTL Sweep</button>
                </div>
            </div>
        </div>
        <h3>Response:</h3>
        <div id="adminJobsTTL_summary" class="text-muted" style="margin-bottom:6px;">-</div>
        <pre id="adminJobsTTL_result">---</pre>
    </div>

    <!-- Requeue Quarantined -->
    <div class="endpoint-section" id="adminJobsRequeueQuarantined">
        <h2>
            <span class="endpoint-method post">POST</span>
            <span class="endpoint-path">/api/v1/jobs/batch/requeue_quarantined - Requeue Quarantined</span>
        </h2>
        <p>Requeue jobs in 'quarantined' status. Scope by current Domain/Queue/Job Type filters.</p>
        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label><input type="checkbox" id="adminJobsRequeue_dryRun" checked> Dry Run (count only)</label>
                </div>
                <div id="adminJobsRequeue_warning" style="display:none; margin:4px 0; padding:6px 8px; border:1px solid #2563eb; border-radius:6px; color:#1e3a8a; background:#eff6ff; font-size:12px;">
                    Heads up: This will move quarantined jobs back to the queue.
                </div>
                <div class="form-group">
                    <button id="adminJobsRequeue_btn" class="btn btn-warning">Requeue Quarantined</button>
                </div>
            </div>
        </div>
        <h3>Response:</h3>
        <div id="adminJobsRequeue_summary" class="text-muted" style="margin-bottom:6px;">-</div>
        <pre id="adminJobsRequeue_result">---</pre>
    </div>

    <!-- Queue Controls & Attachments/SLA -->
    <div class="endpoint-section" id="adminJobsControls">
        <h2>
            <span class="endpoint-method post">POST</span>
            <span class="endpoint-path">Queue Controls / Attachments / SLA</span>
        </h2>
        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label>Domain</label>
                    <input type="text" id="adminJobs_ctrl_domain" placeholder="e.g., prompt_studio">
                </div>
                <div class="form-group">
                    <label>Queue</label>
                    <input type="text" id="adminJobs_ctrl_queue" placeholder="default">
                </div>
                <div class="form-group">
                    <label>Action</label>
                    <select id="adminJobs_ctrl_action">
                        <option value="pause">pause</option>
                        <option value="resume">resume</option>
                        <option value="drain">drain</option>
                    </select>
                </div>
                <div class="form-group">
                    <button class="btn btn-warning" id="btnAdminJobsQueueControl">Apply Queue Control</button>
                    <button class="btn btn-secondary" id="btnAdminJobsQueueStatus">Get Queue Status</button>
                </div>
                <div id="adminJobs_ctrl_status" class="text-muted">-</div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label>Job ID (attachments)</label>
                    <input type="number" id="adminJobs_attach_jobId" placeholder="job id">
                </div>
                <div class="form-group">
                    <label>Add Attachment</label>
                    <input type="text" id="adminJobs_attach_text" placeholder="text or URL">
                    <select id="adminJobs_attach_kind">
                        <option value="log">log</option>
                        <option value="artifact">artifact</option>
                        <option value="tag">tag</option>
                    </select>
                    <button class="btn" id="btnAdminJobsAddAttachment">Add</button>
                </div>
                <div class="form-group">
                    <button class="btn btn-secondary" id="btnAdminJobsListAttachments">List Attachments</button>
                </div>
                <pre id="adminJobs_attach_list">-</pre>
            </div>
            <div class="column">
                <div class="form-group">
                    <label>SLA Policy</label>
                    <input type="text" id="adminJobs_sla_domain" placeholder="domain">
                    <input type="text" id="adminJobs_sla_queue" placeholder="queue">
                    <input type="text" id="adminJobs_sla_jobType" placeholder="job type">
                    <input type="number" id="adminJobs_sla_queueLat" placeholder="max queue latency (s)">
                    <input type="number" id="adminJobs_sla_duration" placeholder="max duration (s)">
                    <label><input type="checkbox" id="adminJobs_sla_enabled" checked> enabled</label>
                </div>
                <div class="form-group">
                    <button class="btn btn-primary" id="btnAdminJobsUpsertSla">Upsert SLA</button>
                    <button class="btn btn-secondary" id="btnAdminJobsListSla">List SLA</button>
                </div>
                <pre id="adminJobs_sla_result">-</pre>
            </div>
        </div>
    </div>

    <!-- Crypto: Rotate Keys -->
    <div class="endpoint-section" id="adminJobsCryptoRotate">
        <h2>
            <span class="endpoint-method post">POST</span>
            <span class="endpoint-path">/api/v1/jobs/crypto/rotate - Rotate Encrypted Payload/Result Keys</span>
        </h2>
        <p>Dry run previews affected rows. Execute requires X-Confirm: true.</p>
        <div class="columns">
            <div class="column">
                <div class="form-group"><label>Domain</label><input type="text" id="adminJobs_crypto_domain" placeholder="(optional)"></div>
                <div class="form-group"><label>Queue</label><input type="text" id="adminJobs_crypto_queue" placeholder="(optional)"></div>
                <div class="form-group"><label>Job Type</label><input type="text" id="adminJobs_crypto_type" placeholder="(optional)"></div>
                <div class="form-group"><label>Fields</label>
                    <label style="margin-right:8px;"><input type="checkbox" id="adminJobs_crypto_payload" checked> payload</label>
                    <label><input type="checkbox" id="adminJobs_crypto_result"> result</label>
                </div>
            </div>
            <div class="column">
                <div class="form-group"><label>Old Key (base64)</label><input type="text" id="adminJobs_crypto_old" placeholder="base64-encoded key"></div>
                <div class="form-group"><label>New Key (base64)</label><input type="text" id="adminJobs_crypto_new" placeholder="base64-encoded key"></div>
                <div class="form-group"><label>Limit</label><input type="number" id="adminJobs_crypto_limit" value="1000" min="1"></div>
                <div class="form-group">
                    <button class="btn btn-secondary" id="btnAdminJobsCryptoDryRun">Dry Run</button>
                    <button class="btn btn-danger" id="btnAdminJobsCryptoExec">Execute (X-Confirm)</button>
                </div>
                <div id="adminJobs_crypto_errors" class="text-error" style="min-height:18px;"></div>
                <pre id="adminJobs_crypto_result">-</pre>
            </div>
        </div>
    </div>

    <!-- Live Job Events -->
    <div class="endpoint-section" id="adminJobsEvents">
        <h2>
            <span class="endpoint-method get">GET</span>
            <span class="endpoint-path">/api/v1/jobs/events/stream - Live Events</span>
        </h2>
        <p>Live job events stream (append-only). Shows recent events and simple failure-timeline sparklines per job.</p>
        <div class="metrics-controls">
            <button id="adminJobsEvents_start" class="btn btn-primary">Start Stream</button>
            <button id="adminJobsEvents_stop" class="btn btn-secondary">Stop</button>
            <label for="adminJobsEvents_max" class="ml-2" style="margin-left:12px;">Max events</label>
            <input type="number" id="adminJobsEvents_max" min="1" value="100" style="width:90px; margin-left:6px;">
        </div>
        <div class="table-responsive mt-2">
            <table class="data-table">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>Event</th>
                        <th>Domain/Queue/Type</th>
                        <th>Failure Timeline</th>
                        <th>Correlation</th>
                        <th>Attrs</th>
                    </tr>
                </thead>
                <tbody id="adminJobsEvents_tableBody">
                    <tr><td colspan="6" class="text-muted">Not connected</td></tr>
                </tbody>
            </table>
        </div>
    </div>

    <script src="js/jobs.js"></script>
    <!-- Inline script migrated to js/jobs.js -->
    <!--
        let adminJobsTimer = null;
        async function adminFetchJobsStats() {
            const domain = document.getElementById('adminJobs_domain').value.trim();
            const queue = document.getElementById('adminJobs_queue').value.trim();
            const jobType = document.getElementById('adminJobs_jobType').value.trim();
            const query = {};
            if (domain) query.domain = domain;
            if (queue) query.queue = queue;
            if (jobType) query.job_type = jobType;
            const tbody = document.getElementById('adminJobs_tableBody');
            try {
                Loading.show(tbody.parentElement, 'Loading jobs stats...');
                const res = await apiClient.makeRequest('GET', '/api/v1/jobs/stats', { query });
                const data = Array.isArray(res) ? res : (res?.data || []);
                tbody.innerHTML = '';
                if (!data.length) {
                    const tr = document.createElement('tr');
                    const td = document.createElement('td');
                    td.colSpan = 6;
                    td.className = 'text-muted';
                    td.textContent = 'No data';
                    tr.appendChild(td);
                    tbody.appendChild(tr);
                    return;
                }
                for (const row of data) {
                    const tr = document.createElement('tr');
                    const values = [
                        row.domain ?? '',
                        row.queue ?? '',
                        row.job_type ?? '',
                        row.queued ?? 0,
                        row.scheduled ?? 0,
                        row.processing ?? 0,
                        row.quarantined ?? 0,
                    ];
                    for (const v of values) {
                        const td = document.createElement('td');
                        td.textContent = String(v);
                        tr.appendChild(td);
                    }
                    // Make rows clickable to apply filter
                    tr.style.cursor = 'pointer';
                    tr.title = 'Click to filter Admin → Jobs';
                    tr.setAttribute('role', 'button');
                    tr.setAttribute('tabindex', '0');
                    tr.addEventListener('click', () => {
                        adminApplyJobsFilter(row.domain || '', row.queue || '', row.job_type || '');
                    });
                    tr.addEventListener('keypress', (e) => {
                        if (e.key === 'Enter' || e.key === ' ') {
                            e.preventDefault();
                            adminApplyJobsFilter(row.domain || '', row.queue || '', row.job_type || '');
                        }
                    });
                    tbody.appendChild(tr);
                }
            } catch (e) {
                const tr = document.createElement('tr');
                const td = document.createElement('td');
                td.colSpan = 6;
                td.className = 'text-error';
                td.textContent = (e && e.message) ? String(e.message) : 'Failed to load';
                tr.appendChild(td);
                tbody.innerHTML = '';
                tbody.appendChild(tr);
            } finally {
                Loading.hide(tbody.parentElement);
            }
        }
        function adminStartJobsAutoRefresh() {
            adminStopJobsAutoRefresh();
            adminJobsTimer = setInterval(adminFetchJobsStats, 10000);
            adminFetchJobsStats();
        }
        function adminStopJobsAutoRefresh() {
            if (adminJobsTimer) {
                clearInterval(adminJobsTimer);
                adminJobsTimer = null;
            }
        }

        // Mini warning banners for destructive actions when Dry Run is unchecked
        function adminInitJobsWarnings() {
            try {
                const pruneCb = document.getElementById('adminJobs_dryRun');
                const pruneWarn = document.getElementById('adminJobsPrune_warning');
                if (pruneCb && pruneWarn) {
                    const syncPrune = () => { pruneWarn.style.display = pruneCb.checked ? 'none' : 'block'; };
                    pruneCb.addEventListener('change', syncPrune);
                    syncPrune();
                }
            } catch (e) { /* no-op */ }
            try {
                const rqCb = document.getElementById('adminJobsRequeue_dryRun');
                const rqWarn = document.getElementById('adminJobsRequeue_warning');
                if (rqCb && rqWarn) {
                    const syncRq = () => { rqWarn.style.display = rqCb.checked ? 'none' : 'block'; };
                    rqCb.addEventListener('change', syncRq);
                    syncRq();
                }
            } catch (e) { /* no-op */ }
        }
        // Initialize warnings on tab load
        adminInitJobsWarnings();

        async function adminJobsQueueControl() {
            const domain = document.getElementById('adminJobs_ctrl_domain')?.value?.trim();
            const queue = document.getElementById('adminJobs_ctrl_queue')?.value?.trim();
            const action = document.getElementById('adminJobs_ctrl_action')?.value;
            const el = document.getElementById('adminJobs_ctrl_status');
            if (!domain || !queue || !action) { el && (el.textContent = 'Missing domain/queue/action'); return; }
            try {
                const res = await apiClient.makeRequest('POST', '/api/v1/jobs/queue/control', { body: { domain, queue, action } });
                el && (el.textContent = `paused=${!!res.paused} drain=${!!res.drain}`);
            } catch (e) {
                el && (el.textContent = (e && e.message) || 'Failed');
            }
        }
        async function adminJobsQueueStatus() {
            const domain = document.getElementById('adminJobs_ctrl_domain')?.value?.trim();
            const queue = document.getElementById('adminJobs_ctrl_queue')?.value?.trim();
            const el = document.getElementById('adminJobs_ctrl_status');
            if (!domain || !queue) { el && (el.textContent = 'Missing domain/queue'); return; }
            try {
                const res = await apiClient.makeRequest('GET', '/api/v1/jobs/queue/status', { query: { domain, queue } });
                el && (el.textContent = `paused=${!!res.paused} drain=${!!res.drain}`);
            } catch (e) {
                el && (el.textContent = (e && e.message) || 'Failed');
            }
        }
        async function adminJobsAddAttachment() {
            const jid = document.getElementById('adminJobs_attach_jobId')?.value;
            const text = document.getElementById('adminJobs_attach_text')?.value;
            const kind = document.getElementById('adminJobs_attach_kind')?.value || 'log';
            if (!jid) return;
            const body = { kind };
            if (kind === 'artifact') body.url = text; else body.content_text = text;
            await apiClient.makeRequest('POST', `/api/v1/jobs/${jid}/attachments`, { body });
            await adminJobsListAttachments();
        }
        async function adminJobsListAttachments() {
            const jid = document.getElementById('adminJobs_attach_jobId')?.value;
            const pre = document.getElementById('adminJobs_attach_list');
            if (!jid) return;
            const res = await apiClient.makeRequest('GET', `/api/v1/jobs/${jid}/attachments`);
            pre && (pre.textContent = JSON.stringify(res, null, 2));
        }
        async function adminJobsUpsertSla() {
            const domain = document.getElementById('adminJobs_sla_domain')?.value?.trim();
            const queue = document.getElementById('adminJobs_sla_queue')?.value?.trim();
            const job_type = document.getElementById('adminJobs_sla_jobType')?.value?.trim();
            const max_queue_latency_seconds = parseInt(document.getElementById('adminJobs_sla_queueLat')?.value || '0', 10);
            const max_duration_seconds = parseInt(document.getElementById('adminJobs_sla_duration')?.value || '0', 10);
            const enabled = !!document.getElementById('adminJobs_sla_enabled')?.checked;
            const body = { domain, queue, job_type, max_queue_latency_seconds, max_duration_seconds, enabled };
            const res = await apiClient.makeRequest('POST', '/api/v1/jobs/sla/policy', { body });
            const el = document.getElementById('adminJobs_sla_result');
            el && (el.textContent = JSON.stringify(res, null, 2));
        }
        async function adminJobsListSla() {
            const domain = document.getElementById('adminJobs_sla_domain')?.value?.trim();
            const queue = document.getElementById('adminJobs_sla_queue')?.value?.trim();
            const job_type = document.getElementById('adminJobs_sla_jobType')?.value?.trim();
            const res = await apiClient.makeRequest('GET', '/api/v1/jobs/sla/policies', { query: { domain, queue, job_type } });
            const el = document.getElementById('adminJobs_sla_result');
            el && (el.textContent = JSON.stringify(res, null, 2));
        }

        function _collectCryptoRotateBody(showErrors = true) {
            const domain = document.getElementById('adminJobs_crypto_domain')?.value?.trim();
            const queue = document.getElementById('adminJobs_crypto_queue')?.value?.trim();
            const job_type = document.getElementById('adminJobs_crypto_type')?.value?.trim();
            const old_key_b64 = document.getElementById('adminJobs_crypto_old')?.value?.trim();
            const new_key_b64 = document.getElementById('adminJobs_crypto_new')?.value?.trim();
            const limit = parseInt(document.getElementById('adminJobs_crypto_limit')?.value || '1000', 10);
            const fields = [];
            if (document.getElementById('adminJobs_crypto_payload')?.checked) fields.push('payload');
            if (document.getElementById('adminJobs_crypto_result')?.checked) fields.push('result');
            const body = { old_key_b64, new_key_b64, fields, limit };
            if (domain) body.domain = domain;
            if (queue) body.queue = queue;
            if (job_type) body.job_type = job_type;
            // inline validation
            const errs = [];
            if (!old_key_b64) errs.push('Old key is required.');
            if (!new_key_b64) errs.push('New key is required.');
            if (!fields.length) errs.push('Select at least one field (payload/result).');
            const box = document.getElementById('adminJobs_crypto_errors');
            if (box && showErrors) box.textContent = errs.join(' ');
            body.__errors = errs;
            return body;
        }
        async function adminJobsCryptoRotateDryRun() {
            const pre = document.getElementById('adminJobs_crypto_result');
            const body = _collectCryptoRotateBody(true);
            if (body.__errors?.length) return;
            body.dry_run = true;
            const res = await apiClient.makeRequest('POST', '/api/v1/jobs/crypto/rotate', { body });
            pre && (pre.textContent = JSON.stringify(res, null, 2));
        }
        async function adminJobsCryptoRotateExecute() {
            const pre = document.getElementById('adminJobs_crypto_result');
            const body = _collectCryptoRotateBody(true);
            if (body.__errors?.length) return;
            body.dry_run = false;
            const res = await apiClient.makeRequest('POST', '/api/v1/jobs/crypto/rotate', { body, headers: { 'X-Confirm': 'true' } });
            pre && (pre.textContent = JSON.stringify(res, null, 2));
        }

        function adminOpenRotateKeysModal() {
            try {
                const content = `
                  <div class="form-grid">
                    <div class="form-group"><label>Domain</label><input type="text" id="rk_domain" placeholder="(optional)"></div>
                    <div class="form-group"><label>Queue</label><input type="text" id="rk_queue" placeholder="(optional)"></div>
                    <div class="form-group"><label>Job Type</label><input type="text" id="rk_type" placeholder="(optional)"></div>
                    <div class="form-group"><label>Old Key (base64)</label><input type="text" id="rk_old" placeholder="base64-encoded key"></div>
                    <div class="form-group"><label>New Key (base64)</label><input type="text" id="rk_new" placeholder="base64-encoded key"></div>
                    <div class="form-group"><label>Fields</label>
                      <label style="margin-right:8px;"><input type="checkbox" id="rk_payload" checked> payload</label>
                      <label><input type="checkbox" id="rk_result"> result</label>
                    </div>
                    <div class="form-group"><label>Limit</label><input type="number" id="rk_limit" value="500" min="1"></div>
                    <div class="form-group"><label>Type 'rotate' to confirm</label><input type="text" id="rk_confirm" placeholder="rotate"></div>
                    <div id="rk_errors" class="text-error" style="min-height:18px;"></div>
                    <pre id="rk_result" style="max-height:200px; overflow:auto;">-</pre>
                  </div>
                `;
                const footer = `
                  <button class="btn btn-secondary" id="rk_dry">Dry Run</button>
                  <button class="btn btn-danger" id="rk_exec" disabled>Execute (X-Confirm)</button>
                `;
                const modal = new Modal({ title: 'Rotate Keys', content, footer, size: 'medium' });
                modal.show();
                const $ = (id) => modal.modal.querySelector(id);
                const collect = () => {
                    const domain = $('#rk_domain').value.trim();
                    const queue = $('#rk_queue').value.trim();
                    const job_type = $('#rk_type').value.trim();
                    const old_key_b64 = $('#rk_old').value.trim();
                    const new_key_b64 = $('#rk_new').value.trim();
                    const limit = parseInt($('#rk_limit').value || '500', 10);
                    const fields = [];
                    if ($('#rk_payload').checked) fields.push('payload');
                    if ($('#rk_result').checked) fields.push('result');
                    const errs = [];
                    if (!old_key_b64) errs.push('Old key is required.');
                    if (!new_key_b64) errs.push('New key is required.');
                    if (!fields.length) errs.push('Select at least one field.');
                    const confirmText = $('#rk_confirm').value.trim().toLowerCase();
                    if (confirmText !== 'rotate') errs.push("Type 'rotate' to enable Execute.");
                    $('#rk_errors').textContent = errs.join(' ');
                    const body = { old_key_b64, new_key_b64, fields, limit };
                    if (domain) body.domain = domain;
                    if (queue) body.queue = queue;
                    if (job_type) body.job_type = job_type;
                    body.__errors = errs;
                    return body;
                };
                // Enable/disable Execute button based on confirmation input
                const updateExecEnabled = () => {
                    const ok = ($('#rk_confirm').value.trim().toLowerCase() === 'rotate');
                    const execBtn = $('#rk_exec');
                    if (execBtn) execBtn.disabled = !ok;
                };
                $('#rk_confirm').addEventListener('input', updateExecEnabled);
                updateExecEnabled();
                $('#rk_dry').onclick = async () => {
                    const body = collect();
                    if (body.__errors.length) return;
                    body.dry_run = true;
                    const res = await apiClient.makeRequest('POST', '/api/v1/jobs/crypto/rotate', { body });
                    $('#rk_result').textContent = JSON.stringify(res, null, 2);
                };
                $('#rk_exec').onclick = async () => {
                    const body = collect();
                    if (body.__errors.length) return;
                    body.dry_run = false;
                    const res = await apiClient.makeRequest('POST', '/api/v1/jobs/crypto/rotate', { body, headers: { 'X-Confirm': 'true' } });
                    $('#rk_result').textContent = JSON.stringify(res, null, 2);
                };
            } catch (e) { console.warn('Rotate Keys modal failed:', e); }
        }

        function adminJobsSaveFilters() {
            try {
                const d = document.getElementById('adminJobs_domain');
                const q = document.getElementById('adminJobs_queue');
                const jt = document.getElementById('adminJobs_jobType');
                const payload = { domain: d ? d.value.trim() : '', queue: q ? q.value.trim() : '', job_type: jt ? jt.value.trim() : '' };
                if (typeof Utils !== 'undefined') Utils.saveToStorage('admin-jobs-filters', payload);
                updateSavedFiltersBadge(payload);
            } catch (e) { /* ignore */ }
        }

        function adminApplyJobsFilter(domain, queue, jobType) {
            try {
                const d = document.getElementById('adminJobs_domain');
                const q = document.getElementById('adminJobs_queue');
                const jt = document.getElementById('adminJobs_jobType');
                if (d) d.value = domain || '';
                if (q) q.value = queue || '';
                if (jt) jt.value = jobType || '';
                const topbar = document.getElementById('adminJobs_topbar');
                if (topbar) {
                    const desc = `${domain || '(any)'}/${queue || '(any)'}/${jobType || '(any)'}`;
                    topbar.textContent = `Filter applied: ${desc}`;
                }
                updateSavedFiltersBadge({ domain, queue, job_type: jobType });
                adminJobsSaveFilters();
                adminFetchJobsStats();
            } catch (e) { /* ignore */ }
        }

        async function adminJobsLoadQueues() {
            try {
                const sel = document.getElementById('adminJobs_queue');
                if (!sel) return;
                // Remove existing dynamic options (keep first (any))
                while (sel.options.length > 1) sel.remove(1);
                const res = await apiClient.makeRequest('GET', '/api/v1/config/jobs', { });
                const queues = (res && res.standard_queues && Array.isArray(res.standard_queues)) ? res.standard_queues : ['default','high','low'];
                queues.forEach(q => {
                    const opt = document.createElement('option');
                    opt.value = q;
                    opt.textContent = q;
                    sel.appendChild(opt);
                });
            } catch (e) {
                // Silent fallback to defaults
            }
        }

        function adminJobsRestoreFilters() {
            try {
                const saved = (typeof Utils !== 'undefined') ? Utils.getFromStorage('admin-jobs-filters') : null;
                if (!saved) return;
                const d = document.getElementById('adminJobs_domain');
                const q = document.getElementById('adminJobs_queue');
                const jt = document.getElementById('adminJobs_jobType');
                if (d) d.value = saved.domain || '';
                if (jt) jt.value = saved.job_type || '';
                if (q) {
                    // Ensure saved option exists before selecting
                    const found = Array.from(q.options).some(o => o.value === (saved.queue || ''));
                    if (!found && saved.queue) {
                        const opt = document.createElement('option');
                        opt.value = saved.queue;
                        opt.textContent = saved.queue;
                        q.appendChild(opt);
                    }
                    q.value = saved.queue || '';
                }
                updateSavedFiltersBadge(saved);
            } catch (e) { /* ignore */ }
        }

        function adminJobsBindPersist() {
            try {
                const ids = ['adminJobs_domain','adminJobs_queue','adminJobs_jobType'];
                ids.forEach(id => {
                    const el = document.getElementById(id);
                    if (el) el.addEventListener('change', adminJobsSaveFilters);
                });
            } catch (e) { /* ignore */ }
        }

        async function adminJobsInit() {
            await adminJobsLoadQueues();
            adminJobsRestoreFilters();
            adminJobsBindPersist();
            try { await adminFetchJobsStats(); } catch (e) { /* ignore */ }
        }

        function updateSavedFiltersBadge(values) {
            try {
                const badge = document.getElementById('adminJobs_savedFiltersBadge');
                if (!badge) return;
                const saved = values || (typeof Utils !== 'undefined' ? Utils.getFromStorage('admin-jobs-filters') : null) || { domain: '', queue: '', job_type: '' };
                const domain = saved.domain || '(any)';
                const queue = saved.queue || '(any)';
                const jobType = saved.job_type || '(any)';
                badge.textContent = `Saved Filters: domain=${domain}, queue=${queue}, job_type=${jobType}`;
            } catch (e) { /* ignore */ }
        }

        function adminJobsResetFilters() {
            try {
                if (typeof Utils !== 'undefined') Utils.saveToStorage('admin-jobs-filters', { domain: '', queue: '', job_type: '' });
                const d = document.getElementById('adminJobs_domain');
                const q = document.getElementById('adminJobs_queue');
                const jt = document.getElementById('adminJobs_jobType');
                if (d) d.value = '';
                if (q) q.value = '';
                if (jt) jt.value = '';
                updateSavedFiltersBadge({ domain: '', queue: '', job_type: '' });
                adminFetchJobsStats();
                Toast.info('Filters reset');
            } catch (e) { /* ignore */ }
        }

        async function adminPruneJobs() {
            const btn = document.getElementById('adminJobsPrune_btn');
            const statuses = Array.from(document.querySelectorAll('.adminJobs_status:checked')).map(cb => cb.value);
            const olderDaysEl = document.getElementById('adminJobs_olderDays');
            const olderThanDays = Math.max(1, parseInt(olderDaysEl && olderDaysEl.value ? olderDaysEl.value : '30', 10));
            const dryRun = !!(document.getElementById('adminJobs_dryRun') && document.getElementById('adminJobs_dryRun').checked);
            const domain = (document.getElementById('adminJobs_domain')?.value || '').trim();
            const queue = (document.getElementById('adminJobs_queue')?.value || '').trim();
            const jobType = (document.getElementById('adminJobs_jobType')?.value || '').trim();
            const resultEl = document.getElementById('adminJobsPrune_result');
            const summaryEl = document.getElementById('adminJobsPrune_summary');
            if (!statuses.length) {
                Toast.error('Select at least one status to prune');
                return;
            }
            const scope = [domain && `domain=${domain}`, queue && `queue=${queue}`, jobType && `job_type=${jobType}`].filter(Boolean).join(', ');
            const preview = dryRun ? ' (dry run)' : '';
            const confirmed = confirm(`Prune${preview} jobs with statuses [${statuses.join(', ')}] older than ${olderThanDays} days${scope ? `, scoped to [${scope}]` : ''}?`);
            if (!confirmed) return;
            try {
                if (btn) btn.disabled = true;
                Loading.show(resultEl, 'Pruning jobs...');
                if (summaryEl) summaryEl.textContent = dryRun ? 'Dry run: counting matching jobs…' : 'Deleting matching jobs…';
                const body = { statuses, older_than_days: olderThanDays, dry_run: dryRun };
                if (domain) body.domain = domain;
                if (queue) body.queue = queue;
                if (jobType) body.job_type = jobType;
                const res = await apiClient.makeRequest('POST', '/api/v1/jobs/prune', { body });
                const deleted = (res && typeof res.deleted === 'number') ? res.deleted : (res?.data?.deleted || 0);
                resultEl.textContent = JSON.stringify(res, null, 2);
                Toast.success(`${dryRun ? 'Would prune' : 'Pruned'} ${deleted} job(s)`);
                const topbar = document.getElementById('adminJobs_topbar');
                if (topbar) {
                    const ts = new Date().toLocaleString();
                    topbar.textContent = `${dryRun ? 'Dry run:' : 'Pruned'} ${deleted} job(s) [${statuses.join(', ')} older than ${olderThanDays} days${scope ? `, ${scope}` : ''}] - ${ts}`;
                }
                if (summaryEl) summaryEl.textContent = dryRun
                    ? `Dry run: would prune ${deleted} job(s) matching filters`
                    : `Deleted ${deleted} job(s)`;
                // Refresh stats after prune
                if (typeof adminFetchJobsStats === 'function') adminFetchJobsStats();
            } catch (e) {
                const err = e && (e.response || e);
                resultEl.textContent = JSON.stringify(err, null, 2);
                Toast.error('Prune failed');
                if (summaryEl) summaryEl.textContent = '-';
            } finally {
                if (btn) btn.disabled = false;
                Loading.hide(resultEl);
            }
        }

        async function adminRunTTLSweep() {
            const btn = document.getElementById('adminJobsTTL_btn');
            const resultEl = document.getElementById('adminJobsTTL_result');
            const summaryEl = document.getElementById('adminJobsTTL_summary');
            const domain = (document.getElementById('adminJobs_domain')?.value || '').trim();
            const queue = (document.getElementById('adminJobs_queue')?.value || '').trim();
            const jobType = (document.getElementById('adminJobs_jobType')?.value || '').trim();
            const ageSecondsStr = (document.getElementById('adminJobs_ttl_age')?.value || '').trim();
            const runtimeSecondsStr = (document.getElementById('adminJobs_ttl_runtime')?.value || '').trim();
            const action = (document.getElementById('adminJobs_ttl_action')?.value || 'cancel');
            const body = { action };
            const ageSeconds = parseInt(ageSecondsStr || '0', 10);
            const runtimeSeconds = parseInt(runtimeSecondsStr || '0', 10);
            if (ageSeconds > 0) body.age_seconds = ageSeconds;
            if (runtimeSeconds > 0) body.runtime_seconds = runtimeSeconds;
            if (domain) body.domain = domain;
            if (queue) body.queue = queue;
            if (jobType) body.job_type = jobType;
            if (!body.age_seconds && !body.runtime_seconds) {
                Toast.error('Provide age_seconds and/or runtime_seconds');
                return;
            }
            try {
                if (btn) btn.disabled = true;
                Loading.show(resultEl, 'Sweeping TTL…');
                if (summaryEl) summaryEl.textContent = 'Running TTL sweep…';
                const res = await apiClient.makeRequest('POST', '/api/v1/jobs/ttl/sweep', { body });
                resultEl.textContent = JSON.stringify(res, null, 2);
                const affected = (res && typeof res.affected === 'number') ? res.affected : (res?.data?.affected || 0);
                Toast.success(`TTL sweep affected ${affected} job(s)`);
                const ts = new Date().toLocaleString();
                const scope = [domain && `domain=${domain}`, queue && `queue=${queue}`, jobType && `job_type=${jobType}`].filter(Boolean).join(', ');
                if (summaryEl) summaryEl.textContent = `TTL ${action} sweep affected ${affected} job(s) [${scope || 'unscoped'}] - ${ts}`;
                if (typeof adminFetchJobsStats === 'function') adminFetchJobsStats();
            } catch (e) {
                const err = e && (e.response || e);
                resultEl.textContent = JSON.stringify(err, null, 2);
                Toast.error('TTL sweep failed');
                if (summaryEl) summaryEl.textContent = '-';
            } finally {
                if (btn) btn.disabled = false;
                Loading.hide(resultEl);
            }
        }

        async function adminRequeueQuarantined() {
            const btn = document.getElementById('adminJobsRequeue_btn');
            const resultEl = document.getElementById('adminJobsRequeue_result');
            const summaryEl = document.getElementById('adminJobsRequeue_summary');
            const domain = (document.getElementById('adminJobs_domain')?.value || '').trim();
            const queue = (document.getElementById('adminJobs_queue')?.value || '').trim();
            const jobType = (document.getElementById('adminJobs_jobType')?.value || '').trim();
            const dryRun = !!document.getElementById('adminJobsRequeue_dryRun')?.checked;
            if (!domain) {
                Toast.error('Provide a domain to scope requeue');
                return;
            }
            try {
                if (btn) btn.disabled = true;
                Loading.show(resultEl, 'Requeuing quarantined…');
                if (summaryEl) summaryEl.textContent = 'Running requeue…';
                const body = { domain, dry_run: dryRun };
                if (queue) body.queue = queue;
                if (jobType) body.job_type = jobType;
                const headers = dryRun ? {} : { 'X-Confirm': 'true' };
                const res = await apiClient.makeRequest('POST', '/api/v1/jobs/batch/requeue_quarantined', { body, headers });
                resultEl.textContent = JSON.stringify(res, null, 2);
                const affected = (res && typeof res.affected === 'number') ? res.affected : (res?.data?.affected || 0);
                Toast.success(`${dryRun ? 'Would requeue' : 'Requeued'} ${affected} job(s)`);
                const ts = new Date().toLocaleString();
                const scope = [domain && `domain=${domain}`, queue && `queue=${queue}`, jobType && `job_type=${jobType}`].filter(Boolean).join(', ');
                if (summaryEl) summaryEl.textContent = `${dryRun ? 'Dry run' : 'Requeued'} ${affected} quarantined job(s) [${scope || 'unscoped'}] - ${ts}`;
                if (typeof adminFetchJobsStats === 'function') adminFetchJobsStats();
            } catch (e) {
                const err = e && (e.response || e);
                resultEl.textContent = JSON.stringify(err, null, 2);
                Toast.error('Requeue failed');
                if (summaryEl) summaryEl.textContent = '-';
            } finally {
                if (btn) btn.disabled = false;
                Loading.hide(resultEl);
            }
        }

        // Initialize on load
        (function(){
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', adminJobsInit);
            } else {
                adminJobsInit();
            }
        })();

        // ---- Live Events stream helpers ----
        let adminJobsEventsAbort = null;
        let adminJobsEventsCursor = 0;
        const adminJobsFailureTimelines = {}; // job_id -> [retry_backoff...]

        function renderSparkline(vals) {
            if (!vals || !vals.length) return '';
            const ticks = ['▁','▂','▃','▄','▅','▆','▇','█'];
            const max = Math.max(...vals);
            if (max <= 0) return '▁'.repeat(Math.min(vals.length, 20));
            return vals.slice(-20).map(v => {
                const idx = Math.min(ticks.length - 1, Math.floor((v / max) * (ticks.length - 1)));
                return ticks[idx];
            }).join('');
        }

        function adminJobsEventsAuthHeaders() {
            const h = { 'Accept': 'text/event-stream' };
            try {
                if (apiClient && apiClient.token) {
                    if (apiClient.authMode === 'multi-user' && !apiClient.preferApiKeyInMultiUser) {
                        h['Authorization'] = `Bearer ${apiClient.token}`;
                    } else {
                        h['X-API-KEY'] = apiClient.token;
                    }
                }
            } catch (e) { /* ignore */ }
            return h;
        }

        function adminStopJobsEvents() {
            try { if (adminJobsEventsAbort) adminJobsEventsAbort.abort(); } catch (e) {}
            adminJobsEventsAbort = null;
        }

        async function adminStartJobsEvents() {
            adminStopJobsEvents();
            const controller = new AbortController();
            adminJobsEventsAbort = controller;
            const tbody = document.getElementById('adminJobsEvents_tableBody');
            if (tbody) {
                const tr = document.createElement('tr');
                const td = document.createElement('td');
                td.colSpan = 6;
                td.className = 'text-muted';
                td.textContent = 'Connecting…';
                tr.appendChild(td);
                tbody.innerHTML = '';
                tbody.appendChild(tr);
            }
            const url = new URL((apiClient?.baseUrl || '') + '/api/v1/jobs/events/stream');
            url.searchParams.set('after_id', String(adminJobsEventsCursor || 0));
            let reader;
            try {
                const res = await fetch(url.toString(), { headers: adminJobsEventsAuthHeaders(), signal: controller.signal });
                if (!res.ok || !res.body) throw new Error('Failed to open stream');
                reader = res.body.getReader();
            } catch (e) {
                if (tbody) {
                    const tr = document.createElement('tr');
                    const td = document.createElement('td');
                    td.colSpan = 6;
                    td.className = 'text-error';
                    td.textContent = (e && e.message) ? String(e.message) : 'Stream failed';
                    tr.appendChild(td);
                    tbody.innerHTML = '';
                    tbody.appendChild(tr);
                }
                return;
            }
            const decoder = new TextDecoder();
            let buf = '';
            const events = [];
            while (true) {
                let chunk;
                try {
                    const { value, done } = await reader.read();
                    if (done) break;
                    chunk = decoder.decode(value, { stream: true });
                } catch (e) { break; }
                buf += chunk;
                const parts = buf.split('\n\n');
                buf = parts.pop();
                for (const part of parts) {
                    const line = part.trim();
                    const prefix = 'data:';
                    if (line.startsWith(prefix)) {
                        try {
                            const obj = JSON.parse(line.slice(prefix.length).trim());
                            // Update cursor via synthetic monotonic counter (table id not exposed in SSE payload)
                            adminJobsEventsCursor += 1;
                            // Track failure timeline from attrs
                            const jid = obj.job_id || obj.attrs?.job_id;
                            const backoff = obj.attrs?.retry_backoff;
                            if (jid && typeof backoff === 'number') {
                                const arr = adminJobsFailureTimelines[jid] || [];
                                arr.push(backoff);
                                adminJobsFailureTimelines[jid] = arr.slice(-20);
                            }
                            events.push(obj);
                        } catch (e) { /* ignore */ }
                    }
                }
                // Render last 20
                if (tbody) {
                    tbody.innerHTML = '';
                    const last = events.slice(-20);
                    for (const ev of last) {
                        const tr = document.createElement('tr');
                        const jid = ev.job_id || '';
                        const tl = adminJobsFailureTimelines[jid] || [];
                        const cells = [];
                        // ID
                        const tdId = document.createElement('td');
                        tdId.textContent = String(jid || '');
                        cells.push(tdId);
                        // Event
                        const tdEvent = document.createElement('td');
                        tdEvent.textContent = String(ev.event || '');
                        cells.push(tdEvent);
                        // Domain/Queue/Type
                        const tdDqt = document.createElement('td');
                        tdDqt.textContent = [ev.domain, ev.queue, ev.job_type].filter(Boolean).join('/');
                        cells.push(tdDqt);
                        // Sparkline
                        const tdSpark = document.createElement('td');
                        tdSpark.style.fontFamily = 'monospace';
                        tdSpark.textContent = renderSparkline(tl);
                        cells.push(tdSpark);
                        // Correlation chips
                        const tdCorr = document.createElement('td');
                        if (ev.request_id) {
                            const s = document.createElement('span'); s.className = 'chip'; s.textContent = `req:${String(ev.request_id)}`; tdCorr.appendChild(s); tdCorr.appendChild(document.createTextNode(' '));
                        }
                        if (ev.trace_id) {
                            const s = document.createElement('span'); s.className = 'chip'; s.textContent = `trace:${String(ev.trace_id)}`; tdCorr.appendChild(s);
                        }
                        cells.push(tdCorr);
                        // Attrs JSON (pretty but safe)
                        const tdAttrs = document.createElement('td');
                        const code = document.createElement('code');
                        code.style.fontSize = '11px';
                        code.textContent = JSON.stringify(ev.attrs || {});
                        tdAttrs.appendChild(code);
                        cells.push(tdAttrs);
                        // Append all
                        for (const td of cells) tr.appendChild(td);
                        tbody.appendChild(tr);
                    }
                    if (!last.length) {
                        const tr = document.createElement('tr');
                        const td = document.createElement('td');
                        td.colSpan = 6;
                        td.className = 'text-muted';
                        td.textContent = 'No events yet';
                        tr.appendChild(td);
                        tbody.appendChild(tr);
                    }
                }
            }
        }
    -->
</div>
